---
id: dbcontext
title: 9.1 数据库上下文
sidebar_label: 9.1 数据库上下文
---

:::warning 连接字符串配置注意事项

如果连接字符串是配置在自定义的 `.json` 文件中，那么必须在 `Visual Studio` 中配置 `.json` 右键属性，设置 `复制` 输出目录为 `如果较新则复制`，生成操作为：`内容`。

否则就会提示找不到配置或连接字符串的错误。

:::

## 9.1.1 数据库上下文

简单来说，数据库上下文是负责和数据库交互的对象，提供程序对数据库存取提供了大量的方法。

在 `Furion` 框架中，默认集成了微软亲儿子：`EntityFramework Core` ，也就是通常数据库上下文指的是 `DbContext` 类或它的实现类。

## 9.1.2 `AppDbContext`

在我们实际项目开发过程中，使用 `EFCore` 提供的 `DbContext` 操作对象操作数据库有些繁琐和复杂，且默认不具备读写分离、多库等操作功能。

所以，`Furion` 框架提供了 `AppDbContext<TDbContext, TDbContextLocator>` 数据库上下文，该上下文继承自 `DbContext`。

:::note 特别说明
后续章节，皆采用 `EFCore` 代替 `EntityFramework Core`。
:::

## 9.1.3 `AppDbContext` 和 `DbContext` 区别

- `AppDbContext` 继承自 `DbContext`，具备 `DbContext` 所有功能。
- `AppDbContext` 支持多数据库操作泛型版本，如：`AppDbContext<TDbContext, TDbContextLocator>`
- `AppDbContext` 自动配置实体信息，无需在 `OnModelCreating` 中配置
- `AppDbContext` 支持内置多租户支持
- `AppDbContext` 支持全局模型配置拦截器
- `AppDbContext` 支持数据提交更改多个事件
- `AppDbContext` 提供更加强大的模型操作能力，如 `Sql` 操作，读写分离等
- `AppDbContext` 能够得到 `Furion` 框架更多的功能支持

## 9.1.4 如何定义数据库上下文

在 `Furion` 框架中了，提供了两种 `AppDbContext` 定义方式：

- `AppDbContext<TDbContext>` 操作默认数据库
- `AppDbContext<TDbContext, TDbContextLocator>` 操作 N 个数据库

其中 `AppDbContext<TDbContext>` 默认继承自 `AppDbContext<TDbContext, TDbContextLocator>`。

下面是数据库上下文创建的多个例子：

### 9.1.4.1 创建默认数据库上下文

```cs {1,6,12}
using Furion.DatabaseAccessor;
using Microsoft.EntityFrameworkCore;

namespace Furion.EntityFramework.Core
{
    [AppDbContext("连接字符串或appsetting.json 键")]
    public class FurionDbContext : AppDbContext<FurionDbContext>  // 继承 AppDbContext<> 类
    {
        /// <summary>
        /// 继承父类构造函数
        /// </summary>
        /// <param name="options"></param>
        public FurionDbContext(DbContextOptions<FurionDbContext> options) : base(options)
        {
        }
    }
}
```

### 9.1.4.2 创建其他数据库上下文

```cs {1,6,12}
using Furion.DatabaseAccessor;
using Microsoft.EntityFrameworkCore;

namespace Furion.EntityFramework.Core
{
    [AppDbContext("连接字符串或appsetting.json 键")]
    public class FurOtherDbContext : AppDbContext<FurOtherDbContext, FurOtherDbContextLocator>  // 继承 AppDbContext<> 类
    {
        /// <summary>
        /// 继承父类构造函数
        /// </summary>
        /// <param name="options"></param>
        public FurOtherDbContext(DbContextOptions<FurOtherDbContext> options) : base(options)
        {
        }
    }
}
```

:::important 特别注意
所有数据库上下文都应该在 `Furion.EntityFramework.Core` 项目中创建。另外如果系统用到了多个数据库，那么从第二个开始必须指定数据库上下文定位器。关于 `TDbContextLocator` 将在下一章节 《[9.2 数据库上下文定位器](dbcontext-locator)》阐述。
:::

## 9.1.5 配置连接字符串

`Furion` 框架提供多种数据库连接字符串配置方式：

- 在注册数据库服务时配置：`AddDbPool<TDbContext>("连接字符串")` 方式
- 使用 `[AppDbContext("连接字符串/Key")]` 特性方式（只在 `AppDbContext 实现类有效`）**推荐**
- 通过重写 `OnConfiguring(DbContextOptionsBuilder optionsBuilder)` 配置

### 9.1.5.1 在注册数据库服务时配置

```cs {12-19} title="Furion.EntityFramework.Core\Startup.cs"
using Furion.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;

namespace Furion.EntityFramework.Core
{
    [AppStartup(600)]
    public sealed class FurEntityFrameworkCoreStartup : AppStartup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            // 配置数据库上下文，支持N个数据库
            services.AddDatabaseAccessor(options =>
            {
                // 配置默认数据库
                options.AddDbPool<FurionDbContext>(DbProvider.SqlServer, connectionString:"连接字符串");

                // 配置多个数据库，多个数据库必须指定数据库上下文定位器
                options.AddDbPool<SqliteDbContext, SqliteDbContextLocaotr>(DbProvider.Sqlite, connectionString:"连接字符串");
            });
        }
    }
}
```

:::caution 新版 MySQL 注意

`MySQL` 在新版本包中注册有所修改，所以注册方式为：

```cs
services.AddDatabaseAccessor(options =>
{
    options.AddDbPool<FurionDbContext>($"{DbProvider.MySql}@8.0.22");
});
```

:::

### 9.1.5.2 `[AppDbContext]` 方式配置

```cs {1,6}
using Furion.DatabaseAccessor;
using Microsoft.EntityFrameworkCore;

namespace Furion.EntityFramework.Core
{
    [AppDbContext("DbConnectionString")]   // 支持 `appsetting.json` 名或 连接字符串
    public class FurionDbContext : AppDbContext<FurionDbContext>
    {
        /// <summary>
        /// 继承父类构造函数
        /// </summary>
        /// <param name="options"></param>
        public FurionDbContext(DbContextOptions<FurionDbContext> options) : base(options)
        {
        }
    }
}
```

:::tip 小提示

`Furion` 推荐使用此方式配置数据库连接字符串。

:::

**`[AppDbContext]`** 内置属性：

- `ConnectionString`：数据库连接字符串，或配置文件中的路径（支持自定义配置查找），或 `appsetting.json` 的 `ConnectionStrings` 配置字符串
- `TablePrefix`：当前数据库上下文表统一前缀
- `TableSuffix`：当前数据库上下文表统一后缀
- `ProviderName`：配置数据库提供器类型，传入 `DbProvider.Xxx`
- `Mode`：配置数据库上下文模式，`DbContextMode` 枚举类型，取值：
  - `Cached`：缓存模型数据库上下文，默认值
  - `Dynamic`：动态模型数据库上下文
- `SlaveDbContextLocators`：主从库配置，设置多个从库定位器，`Type[]` 类型

### 9.1.5.3 `OnConfiguring` 方式配置

```cs {16-20}
using Furion.DatabaseAccessor;
using Microsoft.EntityFrameworkCore;

namespace Furion.EntityFramework.Core
{
    public class FurionDbContext : AppDbContext<FurionDbContext>
    {
        /// <summary>
        /// 继承父类构造函数
        /// </summary>
        /// <param name="options"></param>
        public FurionDbContext(DbContextOptions<FurionDbContext> options) : base(options)
        {
        }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            base.OnConfiguring(optionsBuilder);

            optionsBuilder.UseSqlServer("数据库连接字符串");
        }
    }
}
```

:::important 特别注意

这三种方式可以同时使用，但是有优先级：`[AppDbContext]` -> `在注册数据库服务时配置` -> `OnConfiguring`（低到高）

也就是 `OnConfiguring` 配置会覆盖 `在注册数据库服务时配置` 配置，`在注册数据库服务时配置` 配置会覆盖 `[AppDbContext]` 配置所配置的连接字符串。

:::

### 9.1.5.4 各类数据库连接字符串配置示例

- `Sqlite`：`Data Source=./Furion.db`
- `MySql`：`Data Source=localhost;Database=Furion;User ID=root;Password=000000;pooling=true;port=3306;sslmode=none;CharSet=utf8;`
- `SqlServer`：`Server=localhost;Database=Furion;User=sa;Password=000000;MultipleActiveResultSets=True;`
- `Oracle`：`User Id=orcl;Password=orcl;Data Source=(DESCRIPTION=(ADDRESS_LIST=(ADDRESS=(PROTOCOL=TCP)(HOST=127.0.0.1)(PORT=1521)))(CONNECT_DATA=(SERVICE_NAME=orcl)))`
- `PostgreSQL`：`PORT=5432;DATABASE=postgres;HOST=127.0.0.1;PASSWORD=postgres;USER ID=postgres;`

## 9.1.6 数据库上下文定义位置

:::important 特别注意

在 `Furion` 框架中，数据库上下文需定义在 `Furion.EntityFramework.Core` 中，且每一个数据库上下文都必须拥有唯一的 `DbContextLocator` 定位器

:::

## 9.1.7 数据库上下文注册

数据库上下文配置好数据库连接字符串后，需要注册该数据库上下文，并指定数据库类型，如：

```cs {11-13} title="Furion\framework\Furion.EntityFramework.Core\FurEntityFrameworkCoreStartup.cs"
using Furion.DatabaseAccessor;
using Microsoft.Extensions.DependencyInjection;

namespace Furion.EntityFramework.Core
{
    [AppStartup(600)]
    public sealed class FurEntityFrameworkCoreStartup : AppStartup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDatabaseAccessor(options =>
            {
                options.AddDbPool<FurionDbContext>(DbProvider.Sqlite);
            });
        }
    }
}
```

如果有多个数据库操作，**那么从第二个起，就需要绑定数据库上下文定位器**，如：

```cs
options.AddDbPool<FurionDbContext>(DbProvider.Sqlite); // 第一个数据库

options.AddDbPool<SecondDbContext, SecondDbContextDbContextLocator>(DbProvider.SqlServer);  // 第二个数据库

options.AddDbPool<ThirdDbContext, ThirdDbContextDbContextLocator>(DbProvider.SqlServer);  // 第三个数据库
```

## 9.1.8 自定义高级注册

在 `Furion` 框架中，为了能够实现数据库的简单使用进行了注册封装，但有些时候，我们可能需要添加更多配置，这时就需要使用原生自定义配置方式，如：

```cs
// 注册你的数据库上下文和定位器绑定
services.AddDatabaseAccessor(options =>
{
    // 注册数据库上下文和定位器
    options.RegisterDbContext<YourDbContext, YourDbContextLocator>();

    // 获取连接字符串
    var connStr = DbProvider.GetConnectionString<YourDbContext>(/*这里可写可不写*/);

    // 使用原生注册
    options.AddDbContextPool<YourDbContext>(builder =>
    {
        // 注册特定数据库
        builder.UseSqlite(connStr, dbContextOptions =>
        {
            // 配置迁移程序集
            dbContextOptions.MigrationsAssembly("迁移程序集");
        }).EnableDetailedErrors()   // 启动详细错误
          .EnableSensitiveDataLogging() // 替换参数为具体参数值
          .AddInterceptors(DbProvider.GetDefaultInterceptors()); // 添加默认过滤器
    }, 100);    // 配置数据库连接池
});

```

## 9.1.9 动态数据库上下文对象

在 `Furion` 框架中，数据库上下文是定义在 `Furion.EntityFramework.Core` 项目层，并且该层不被 `Furion.Application` 和 `Furion.Core` 等层引用。

所以就不能直接在 `Furion.Application` 项目层直接使用 `Furion.EntityFramework.Core` 定义的数据库上下文。

`Furion` 为了解决这个问题，提供了两种方式处理：

- `respository.Context` ：当前数据库上下文对象，返回是 `DbContext` 抽象类型
- `respository.DynamicContext`：当前数据库上下文对象，返回的是 `dynamic` 类型

如果你只是想使用 `DbContext` 的功能，直接使用 `respository.Context` 即可，如：

```cs
respository.Context.SaveChanges();
```

如果你想能够获取具体的数据库上下文类型，如 `MyDbContext`，那么使用 `respository.DynamicContext` 就可以获取到具体的 `MyDbContext` 类型。如：

```cs
var persons = respository.DynamicContext.Persons.Find(1);
var users = respository.DynamicContext.Users;
```

这样就可以直接操作 `MyDbContext` 定义的属性和方法了。

## 9.1.10 在后台任务中使用

由于 `DbContext` 默认注册为 `Scoped` 生存周期，所以在后台任务中使用 `IServiceScopeFactory` 获取所有服务，如：

```cs
public class JobService : BackgroundService
{
    // 日志对象
    private readonly ILogger<JobService> _logger;

    // 服务工厂
    private readonly IServiceScopeFactory _scopeFactory;
    public JobService(ILogger<JobService> logger
        , IServiceScopeFactory scopeFactory)
    {
        _logger = logger;
        _scopeFactory = scopeFactory;
    }

    protected override Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("写日志~~");

        using (var scope = _scopeFactory.CreateScope())
        {
            var services = scope.ServiceProvider;

            // 获取数据库上下文
            var dbContext = Db.GetDbContext(services);

            // 获取仓储
            var respository = Db.GetRepository<Person>(services);

            // 解析其他服务
            var otherService = services.GetService<XXX>();
        }

        return Task.CompletedTask;
    }
}
```

:::important 数据库操作注意

如果作用域中对**数据库有任何变更操作**，需手动调用 `SaveChanges` 或带 `Now` 结尾的方法。也可以使用 `Scoped.CreateUow(handler)` 代替。

:::

## 9.1.11 `AppDbContext` 全局配置属性

- `InsertOrUpdateIgnoreNullValues`：新增或更新忽略空值，默认 `false`，**在构造函数中配置**
- `EnabledEntityStateTracked`：启用实体跟踪，默认 `true`，**在构造函数中配置**
- `EnabledEntityChangedListener`：启用实体数据更改监听，默认 `false`，**在构造函数中配置**
- `Tenant`：默认内置多租户
- `FailedAutoRollback`：是否启用保存失败后事务自动回滚，默认 `true`，可以在任何地方配置

## 9.1.12 配置实体 `懒加载`

- 第一步：安装 `EFCore` 拓展包

在数据库上下文定义所在的层安装 `Microsoft.EntityFrameworkCore.Proxies` 拓展包

- 第二步：采用 `AddDb<TDbContext>` 方式注册

确保数据库上下文采用 `AddDb<TDbContext>` 注册而非 `AddDbPool<TDbContext>`。

- 第三步：重写 `OnConfiguring` 方法

```cs {13,15}
using Furion.DatabaseAccessor;
using Microsoft.EntityFrameworkCore;

namespace Furion.EntityFramework.Core
{
    [AppDbContext("Sqlite3ConnectionString", DbProvider.Sqlite)]
    public class DefaultDbContext : AppDbContext<DefaultDbContext>
    {
        public DefaultDbContext(DbContextOptions<DefaultDbContext> options) : base(options)
        {
        }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseLazyLoadingProxies()
                          .UseSqlite(DbProvider.GetConnectionString<DefaultDbContext>());

            base.OnConfiguring(optionsBuilder);
        }
    }
}
```

:::note 小知识

更多 `EFCore` 懒加载可查看 **[【EFCore - 延迟加载】](https://docs.microsoft.com/zh-cn/ef/core/querying/related-data/lazy)** 文档。

:::

## 9.1.13 反馈与建议

:::note 与我们交流

给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。

:::

---

:::note 了解更多

想了解更多 `数据库上下文` 知识可查阅 [EF Core - 配置 DbContext](https://docs.microsoft.com/zh-cn/ef/core/miscellaneous/configuring-dbcontext) 章节。

:::
